Flutter 使用 freezed 管理数据类

在 App 开发过程中,通常需要声明大量数据实体类(Model/Entity),最典型场景是网络请求数据数据。在 Flutter/Dart 下,因为无法使用反射,封装数据实体类不是一件容易事。社区的最佳实践是使用代码生成器,用户声明实体的主要内容,由生成器补全周边实现。freezed 是这一领域比较知名的框架,在本文中,记录了我对该框架的了解上手。

从官方文档可知,freezed 的特色体现在以下几个方面:

别小看实体类,不仅仅是“JSON 转 Model”那么简单。考虑好上面几方面因素,将让软件项目的代码质量、架构质量上升一个台阶。

安装

这里以 Flutter 项目为例,需要安装以下依赖:

flutter pub add freezed_annotation
flutter pub add dev:build_runner
flutter pub add dev:freezed

如果需要 JSON 序列化转换,还需要安装 json_annotation

# if using freezed to generate fromJson/toJson, also add:
flutter pub add json_annotation
flutter pub add dev:json_serializable

其中:

仅在开发是使用的依赖:

会编译到产物中的代码:

这里以官方 Example 为例。

声明实体类

生成实体类

创建名为 Person 的实体类,创建 person.dart

Pasted image 20240103165734.png

之所以使用截图来展示代码,是因为在写 Person 类的代码时,会有许多标红找不到。这是正常情况,因为这些标红的,正是需要生成的。

这里有些固定写法需要注意:

这种固定写法,是需要开发者记忆或者通过工具来生成的。并且在书写时,没有代码提示,全靠记忆。这些是无法避免的 boilerplate code。

执行代码生成器

首先,需要通过 build_runner 执行代码生成器:

dart run build_runner build

执行后,将看到两个 part 依赖文件都被生成出来,报错也都自动消除了。

编辑器插件

在上一节中说道,尽管 freezed 降低了 boilerplate code,但是仍然存在一些固定写法,在记忆上造成一些负担。为此,freezed 还提供了编辑器插件(VSCodeIntelliJ、Android Studio)。

由于我使用 VSCode,这里以 VS Code 为例。

在 VS Code 下,需要安装两个插件:

前者负责生成 freezed 类,能够自动生成 boilerplate code,将我们彻底解放。

后者负责触发 build runner,省去了自己在命令行里输入的繁琐。

在以上两个插件的加持上,开发体验非常爽了。

Immutability

freezed 提供了两个注解,分别用于声明不可变和可变对象:

本节介绍 @freezed 不可变实体类。

所谓不可变实体类,创建实例后,属性不可修改:

Pasted image 20240103170708.png

不仅如此,如果 @freezed 对象的属性为集合类型(List、Map、Set),它们也是不可修改的。

总之,实例创建出来之后,不可修改,修改就抛异常

判等

看下面代码:

Person p1 = Person(firstName: "Maxiee", lastName: "Maeiee", age: 18);
// 可以复制并修改新对象的值
Person p2 = p1.copyWith(); 

print("p1 == p2: ${p1 == p2}"); // true

p1 == p2 返回 true。首先,他俩不是同一个实例。之所以相等,是因为代码生成器重写了判等逻辑:

// PersonImpl in person.freezed.dart
@override
bool operator ==(Object other) {
  return identical(this, other) ||
      (other.runtimeType == runtimeType &&
          other is _$PersonImpl &&
          (identical(other.firstName, firstName) ||
              other.firstName == firstName) &&
          (identical(other.lastName, lastName) ||
              other.lastName == lastName) &&
          (identical(other.age, age) || other.age == age));
}

从中可见,不仅实例相同会认为相等,不同实例的值相等也会认为相等。

这意味着,p2 是从 p1 深拷贝出来的,但两者仍然能够比较。

深拷贝

说到深拷贝,通过 copyWith 方法实现,该方法允许接受参数,允许对成员进行修改。

上一节代码中,给出了 copyWith 的基本使用方法。

在官方文档中,给出了一个高级用例,即当有层层实体类嵌套时:

@freezed
class Company with _$Company {
  factory Company({
	  String? name, 
	  required Director 
	  director}) = _Company;
}

@freezed
class Director with _$Director {
  factory Director({
	  String? name, 
	  Assistant? assistant}) = _Director;
}

@freezed
class Assistant with _$Assistant {
  factory Assistant({
	  String? name, 
	  int? age}) = _Assistant;
}

我们想更换 Company 的 Director 的 Assistant,该怎么办?伪代码如下:

Company company;

Company newCompany = company.copyWith.director.assistant(name: 'John Smith');

如果只想更换 Director 的 name?

Company company;
Company newCompany = company.copyWith.director(name: 'John Doe');

其他用法

为实体类添加方法

需要手动添加一个私有构造方法:

@freezed
class Person with _$Person {
  // Added constructor. Must not have any parameter
  const Person._();

  const factory Person(String name, {int? age}) = _Person;

  void method() {
    print('hello world');
  }
}

构造断言

class Person with _$Person {
  @Assert('name.isNotEmpty', 'name cannot be empty')
  @Assert('age >= 0')
  factory Person({
    String? name,
    int? age,
  }) = _Person;
}

默认值

class Example with _$Example {
  const factory Example([
	  @Default(42) int value]) = _Example;
}

json_serializable 是打通的。

更复杂用法

更复杂用法还包括 Union 联合类型、泛型。我还没有研究到这么深入,请参见官方文档。

总结

freezed 还支持 @unfreezed Mutable 类型,本文中不再赘述,感兴趣的小伙伴可参见官方文档。

网络资源


本文作者:Maeiee

本文链接:Flutter 使用 freezed 管理数据类

版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!


喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!